iT邦幫忙

2026 iThome 鐵人賽

DAY 8
0
ChatGPT & Codex

AI 救得了祖傳系統嗎?30 天實戰企業 Legacy System × AI 協作開發系列 第 8 篇

Day 8|流水號從三碼改四碼,為什麼不能只改欄位?

  • 分享至 

  • xImage
  •  

前言/情境導入

每個接到「把編號從三碼改成四碼」需求的人,大概都曾經以為:這個半小時可以做完。

這次收到的需求也只有一句:

「把流水號從三碼改成四碼。」

第一眼看到時,我腦中浮現的修改大概是:

CHAR(3) → CHAR(4)

畫面輸入框的 MaxLength 改成 4,Delphi Dataset Field 的 Size 改成 4,報表欄位不夠寬就再拉開一點。照這份清單往下做,看起來半小時真的可以收工。

但我沒有立刻動手。

因為這個流水號不是只顯示在畫面上的一段字串。每新增一批工料資料,主檔會記住它,明細會靠它找到自己屬於哪一批,歷史資料會拿它判斷先後,查詢與報表也會把它當成篩選或排序條件。

它還會一路流進簽核、匯出與其他模組的回查流程。下一次新增時,系統又必須從既有資料裡找出「目前最後一號」,再產生下一個號碼。

換句話說,如果流水號的意義判斷錯了,受影響的可能不只是一個輸入框。主檔與明細可能對不起來,歷史順序可能排錯,報表可能漏資料,下一號也可能從錯誤的位置繼續往下編。

所以這次我沒有直接把所有 Size = 3 改成 4,而是先去確認:

這三個字元,過去到底是怎麼被產生的?

原本我以為答案應該很單純。既然是流水號,大概就是:

001 → 002 → 003 → ... → 998 → 999

但實際往歷史資料裡看,事情開始變得奇怪。

除了熟悉的數字,我還看到了:

3E8
3E9
3EA

流水號裡怎麼會出現英文字母?

這時問題已經不再是「欄位能不能放四碼」,而是:

這套系統過去到底用什麼規則理解這個流水號?

也就是從這裡開始,原本看似半小時可以完成的小修改,正式變成一場 Legacy System 的考古。


工程師往回查,才找到真正的歷史規則

我沿著產號程式與歷史資料往回追,才確認舊系統並不是從第一筆開始就使用十六進位。

一開始的 001~999 仍是一般十進位:

001
002
003
...
998
999

但欄位只有三碼。當年數到 999 後,系統沒有立即擴充欄位,而是改用三碼十六進位延續數值:

999
3E8  ← 十進位 1000
3E9  ← 十進位 1001
3EA  ← 十進位 1002

三碼十六進位最多可以表示到:

FFF = 十進位 4095

現在的新規則則是四碼十進位,只使用 0~9。

舊制
001 ───────────── 999 │ 3E8 ───────────── FFF
     三碼十進位       │      三碼十六進位
                      ↓
                   共同數值
                      ↓
新制
0001 ───────────────────────────── 9999
                 四碼十進位

因此,資料不能只用「三碼或四碼」判斷,更不能看到三碼就全部套用同一個進位制。

儲存字串 所屬規則 共同數值 新制下一號
295 999 以前的十進位區段 295 0296
999 十進位區段上限 999 1000
3E8 十六進位延伸區段 1000 1001
FFF 三碼十六進位上限 4095 4096

但追到這裡,還有一個不能跳過的識別問題。

十六進位從 3E8 往後增加,接著會經過:

3FE
3FF
400  ← 十六進位 1024
401

問題是,字串 400 早就在前面的十進位階段出現過,當時代表十進位 400。也就是說,同一個三碼字串可能具有兩種語意:

儲存字串 可能的歷史語意 共同數值
400 早期十進位流水號 400
400 後期十六進位延伸值 1024

如果兩筆資料位於同一個流水號作用域,這甚至可能造成鍵值碰撞;如果舊系統是依工程、案件、建立順序、建立時間或其他欄位區分,就必須把那些 Context 一起納入判斷。

目前我只能確認系統曾使用這兩段規則,還不能只靠流水號字串證明每一筆資料屬於哪一段,也不能替舊系統假設一個尚未找到的防撞機制。

因此,這張圖描述的是歷史規則的演變,不是一個可以單靠字串執行的分類器。後續程式、SQL 與測試若要轉成共同數值,必須先取得案件範圍、建立順序或其他已確認的歷史 Context。

Legacy System 最麻煩的不是格式混在一起,而是同一個字串在不同歷史階段,可能代表不同的東西。


我原本以為找到答案,卻又看到 295 → 647

確認 001~999 是十進位、超過 999 才改用十六進位後,我原本以為問題已經解釋完了。

只要把兩套規則轉成共同數值,再輸出四碼十進位,不就結束了?

但接著,我在另一筆歷史資料裡看到了這個結果:

正常預期:295 → 296
實際資料:295 → 647

這時事情又變了。

如果只是十進位與十六進位的切換,295 不應該跳到 647。這代表系統裡可能還存在另一個尚未理解的因素。

目前能確認的事實只有兩個:

  1. 某些歷史資料確實曾經大幅跳號;
  2. 既有的十進位/十六進位規則無法單獨解釋這次跳號。

至於原因,當時都還只是待驗證假設:

  • 字串排序找錯最後一號;
  • 十進位與十六進位轉換錯誤;
  • 人工輸入或人工修改;
  • 歷史資料曾經被修補;
  • 系統裡存在不只一套產號程式;
  • 多人同時新增時讀到相同基準。

例如下面這種查詢看似直覺:

SELECT MAX(ITEM_NO)
FROM ITEM_HEADER;

但資料庫比較的是字串順序,不是流水號的共同數值。混有 0999、1000 與 FFF 時,字串最大值未必是數值最大值,也未必是最後建立的資料。

只找到字串最大的編號,不代表找到數值最大或最後建立的編號。

不過,這仍然只是一個可查證方向,不能看到 MAX 就宣布它是 295 → 647 的真正原因。

這就是 Legacy System 維護最熟悉的節奏:以為已經找到答案,五分鐘後又出現一筆資料告訴你——考古還沒結束。


只告訴 AI「有十六進位」,反而更危險

這時如果我只把部分資訊交給 AI:

舊系統的流水號曾經使用三碼十六進位,
現在要改成四碼十進位。

這句話本身沒有錯,但它少了一個最關鍵的條件:

十六進位不是從第一筆資料就開始使用,而是超過 999 後才加入的延伸規則。

少掉這個 Context,AI 很容易把局部規則概括成全域規則。

例如我再問:

「目前最後一號是 295,改成四碼後,下一號是多少?」

如果把所有三碼舊資料都當成十六進位,就可能得到:

0x295 = 661
下一號 = 662
四碼輸出 = 0662

數學沒有算錯。

但系統答案錯了。

因為按照剛才確認的歷史規則,295 還位於 001~999 的十進位區段。真正應該得到的是:

295
 ↓
296
 ↓
0296

這個例子真正提醒我的,不是「AI 連進位制都會算錯」。恰恰相反,AI 可以把進位制算得完全正確,卻因為不知道企業系統的歷史分界,非常精確地套用了一條錯誤規則。

資料可以成功轉型,不代表轉型方式正確。

295 → 0662 是不完整 Context 可能造成的誤判;295 → 647 則是 Production 歷史資料中真正出現、仍需要追查的異常。兩者不能混為一談。

情境 結果 性質
正確歷史規則 295 → 0296 應有行為
AI 誤套十六進位 295 → 0662 假設錯誤
Production 歷史資料 295 → 647 尚未釐清的異常

讓 AI 協助安排調查順序,而不是替我猜答案

面對 295 → 647,我沒有只問 AI:「為什麼會跳號?」

這種問法很容易得到一串聽起來都合理、卻無法直接採用的答案。我改成先提供已確認的事實,再要求 AI 按照證據來源拆分調查方式:

目前已確認:

1. 001~999 為三碼十進位。
2. 超過 999 後,才使用三碼十六進位延續。
3. 某些歷史資料曾出現目前值 295,
   下一筆卻跳到 647 的情況。
4. 十六進位延伸後可能再次出現 400、401 等純數字字串,
   不能只靠流水號外觀判斷格式。
5. 目前不知道跳號原因,也尚未確認舊系統如何避免或區分重疊值。

請不要猜測單一答案,也不要直接產生修改程式。

請將可能原因分成:
A. 可以從程式碼驗證
B. 可以從資料庫資料驗證
C. 需要操作重現
D. 無法從目前證據判定

請列出每一類應先檢查的位置、需要的證據,
並區分「已確認事實」、「程式推論」與「待驗證假設」。

這時 AI 的價值不再是替我選一個最像答案的原因,而是協助建立調查順序:

  • 從程式碼搜尋所有產號入口與格式轉換;
  • 從資料庫比對建立順序、流水號與異常案件;
  • 從實際操作確認新增流程是否還有其他入口;
  • 把現有證據不足的部分明確保留為未知。

AI 能讀懂眼前的 Code,卻不會自動知道企業系統多年演變留下來的背景。那些沒有寫進註解、只存在資料與維護經驗裡的歷史,必須由工程師補成 Context,它才有機會做出符合現場的分析。

工料流水號也可能同時是顯示值、關聯鍵、查詢條件與排序依據。只搜尋到欄位名稱仍不夠,還要確認每個位置究竟拿它做什麼。


工程師判決:部分接受,但不能讓 AI 補完商業邊界

AI 適合協助:

  • 搜尋欄位與影響範圍;
  • 整理轉換函式;
  • 找出字串排序風險;
  • 產生邊界測試案例;
  • 比較修改前後的程式差異。

但以下決策仍要由工程師確認:

  1. 舊資料的格式判斷規則;
  2. 新制從哪個條件開始生效;
  3. 歷史資料是否原地轉換;
  4. 原因不明的跳號要阻擋、警告還是沿用;
  5. 下一號是全系統共用,還是依工程分組;
  6. 哪一層擁有最終產號權。

這不是因為 AI 完全不可靠,而是分工不同:AI 負責協助分析,工程師負責確認商業邊界。

這次我採用的方向是:

  • 舊資料保持原樣,不全面改寫;
  • 新增資料統一使用四碼十進位;
  • 比較與產號前,先把舊制轉成共同數值;
  • 產號移到資料庫集中管理;
  • 新規則驗證新增資料,不阻擋舊資料的正常修改;
  • 原因未明的跳號不自動修正,先留下可追查的證據。

💡 工程師判決:欄位長度只是表面,資料語意、歷史分界與相容性才是改版真正的成本。


真正開始改時,我才發現不是只有一個欄位

規則確認完之後,事情才正式進入 Legacy System 最花時間的部分:盤點這個流水號到底穿過哪些地方。

我原本以為搜尋欄位名稱,就能找到所有修改點。實際追下去才發現,同一個值在不同程式裡可能有不同名稱,也可能被放進參數、暫存變數或共用函式,再一路傳到其他 Dataset。

最後整理出的修改路徑大致是:

資料庫欄位
    ↓
Delphi Dataset Field
    ↓
畫面輸入元件
    ↓
Query Parameter/暫存變數
    ↓
產號程式
    ↓
主檔/明細傳值
    ↓
查詢與排序
    ↓
報表/簽核/匯出
    ↓
歷史資料相容

這不是把每個地方都從 3 改成 4 就結束。每一層使用流水號的目的不同,需要確認的風險也不一樣。

Dataset Field 能放四碼,不代表四碼走得到終點

在 Delphi 裡,把 Dataset Field 的 Size 從 3 改成 4,只能證明這一層願意接受四個字元。

假設主畫面的 Dataset 已經改成 4,但另一個查詢 Dataset 仍保留 3;或是 Query Parameter、暫存變數、共用函式仍假設流水號只有三碼,那麼 0296 可能在流程途中被截斷、轉成整數後失去前導零,甚至在重新查詢時組出不同條件。

畫面上的 MaxLength 也是一樣。它只負責限制使用者能輸入幾個字,並不會自動修正資料庫欄位、參數型別或報表資料來源。

所以盤點時不能只問:

哪些欄位的 Size 是 3?

還要繼續問:

這個值接下來被傳到哪裡?中途有沒有被轉型、補零、截斷或重新組合?

主檔與明細必須拿到完全相同的字串

如果流水號同時是主檔與明細的關聯值,兩邊就不能各自理解「同一個號碼」。

下面這種結果在人眼看來似乎都代表二百九十六:

主檔:0296
明細:296

但對資料庫而言,0296 與 296 是兩個不同的字串。若它們參與複合鍵、查詢條件或關聯,明細可能因此查不到主檔,報表也可能出現只有表頭、沒有內容的情況。

因此,新的流水號產生後,應該由同一個來源傳給主檔與所有明細,而不是讓各畫面或各 Dataset 再自行補零、轉型一次。

流水號不只要數值相同,儲存格式也必須完全一致。

查詢與排序不能繼續把它當普通字串

欄位擴成四碼後,舊資料並不會憑空消失。查詢結果裡仍可能同時出現 295、3E8、0296 與 1000。

如果某段程式仍直接依字串排序,或拿新制的四碼條件去比較舊制三碼資料,即使新增功能已經能存檔,歷史資料的順序與篩選結果仍可能錯誤。

這也是為什麼前面要先建立「共同數值」。它不只用來產生下一號,也用來讓跨格式的比較有一致語意。

關鍵實作:先轉成共同數值

以下是匿名化的簡化範例,不直接對應任何公司資料表。

這段程式有一個必要前提:呼叫端已經依案件範圍、建立順序或其他已確認的歷史條件,將資料判定為 LEGACY_HEX。它不能看到三碼字串就自行宣布這筆資料是十六進位。

DECLARE @LegacyNo varchar(10) = '3E8';
DECLARE @FormatStatus varchar(20) = 'LEGACY_HEX';
DECLARE @HexDigits varchar(16) = '0123456789ABCDEF';

SET @LegacyNo = UPPER(LTRIM(RTRIM(@LegacyNo)));

-- @FormatStatus 必須由已確認的歷史 Context 產生,
-- 不能只依 @LegacyNo 的長度或字元外觀決定。
IF @FormatStatus <> 'LEGACY_HEX'
BEGIN
    THROW 50009, N'此筆資料尚未確認為十六進位延伸格式。', 1;
END;

IF LEN(@LegacyNo) <> 3
   OR CHARINDEX(SUBSTRING(@LegacyNo, 1, 1), @HexDigits) = 0
   OR CHARINDEX(SUBSTRING(@LegacyNo, 2, 1), @HexDigits) = 0
   OR CHARINDEX(SUBSTRING(@LegacyNo, 3, 1), @HexDigits) = 0
BEGIN
    THROW 50010, N'舊制流水號包含無效的十六進位字元。', 1;
END;

DECLARE @DecimalValue int;

SET @DecimalValue =
      (CHARINDEX(SUBSTRING(@LegacyNo, 1, 1), @HexDigits) - 1) * 256
    + (CHARINDEX(SUBSTRING(@LegacyNo, 2, 1), @HexDigits) - 1) * 16
    + (CHARINDEX(SUBSTRING(@LegacyNo, 3, 1), @HexDigits) - 1);

SELECT @DecimalValue AS DecimalValue;
-- 1000

這裡分成兩層防護:先確認歷史分類確實是 LEGACY_HEX,再驗證字串長度與每一個字元。如果把 295 直接丟進換算公式,數學上仍會得到 661,所以真正阻止誤判的不是公式,而是前面的 Context 分類。

如果髒資料找不到對應字元,CHARINDEX 會回傳 0;若沒有字元防呆,後續的 -1 也會悄悄參與計算,產生另一種看似正常的錯誤結果。

取得共同數值後,再依新制輸出下一號:

DECLARE @NextValue int = @DecimalValue + 1;

IF @NextValue > 9999
BEGIN
    THROW 50011, N'流水號已超過四碼十進位上限。', 1;
END;

SELECT RIGHT('0000' + CONVERT(varchar(4), @NextValue), 4) AS NextItemNo;
-- 1001

真正重要的不是公式多漂亮,而是順序不能反過來:

辨識歷史規則
→ 轉成共同數值
→ 計算下一號
→ 輸出四碼十進位

不要直接比較兩種格式的字串;先把它們轉成共同數值,再討論大小與下一號。

在正式轉換前,我只會先用唯讀查詢盤點資料,不會一開始就執行 UPDATE。這類使用字串函式與萬用字元的盤點查詢,放到 Production 前仍須確認資料量與 Execution Plan,避免大量掃描影響線上系統。


為什麼我沒有直接 UPDATE 所有歷史資料?

最乾脆的作法看起來是:把舊資料全部轉成四碼十進位,從此只維護一種格式。

但流水號可能早已存在於主檔、明細、歷史資料、簽核紀錄、報表條件、附件名稱與其他模組。如果其中任何一處沒有一起更新,就可能留下找不到主檔的明細、對不到來源的簽核紀錄,或再也無法由舊條件找到的附件。

更重要的是,現有資料裡還有原因未明的跳號與人工修補痕跡。若在規則尚未完全釐清前全面轉換,原始值會被覆蓋,反而失去追查問題的重要證據。

所以這次採取的策略是:

舊資料:維持原值,依歷史規則解讀
新資料:使用四碼十進位

這不是因為懶得轉換,而是在相容性、可追查性與改版風險之間做出的選擇。等到歷史資料的關聯範圍與異常來源都確認後,再評估是否需要另做資料遷移,會比直接在 Production 執行一場大規模 UPDATE 安全得多。


改完欄位,不代表改完功能

資料庫成功存入 0296,只能證明資料表容得下這四個字元,還不能證明整個功能已經改完。

真正需要驗證的是完整操作流程:

新增主檔
    ↓
取得四碼流水號
    ↓
新增明細
    ↓
儲存
    ↓
重新查詢
    ↓
修改
    ↓
簽核
    ↓
報表/匯出
    ↓
再次讀取歷史舊制資料

我會特別確認三件事:

  1. 寫入後沒有變形:主檔與明細保存的都是 0296,不會有一邊遺失前導零。
  2. 重新讀取後仍能定位同一筆資料:關閉畫面再查詢,明細、歷史與簽核內容仍能正確回來。
  3. 新舊資料可以在同一套功能裡共存:新增的四碼資料能走完整流程,舊制三碼資料也仍能查詢、修改與列印。

真正要驗證的不是資料庫能不能存入 0296,而是 0296 經過整條系統流程後,仍然被每一層當成同一個流水號。

這種 End-to-End 驗證,也是 AI 很容易漏掉的部分。AI 可以從單一 SQL 或單一事件判斷程式是否合理,但實際系統是否在畫面切換、重新查詢、簽核與報表之間保持一致,仍需要工程師依照真實操作流程逐段確認。


最後,我真正改了哪些東西?

追完資料流、確認新舊規則後,這次真正落地的修改可以分成三層:

Delphi
├─ Dataset Field:3 → 4
├─ 畫面輸入長度:3 → 4
├─ 查詢參數與傳值:確認不截斷、不遺失前導零
└─ Client 不再自行決定下一號

SQL Server
├─ 流水號欄位可容納四碼
├─ 舊制資料先轉成共同數值判斷
├─ 新增資料統一產生四碼十進位
└─ 新資料的格式驗證集中在資料庫

歷史資料
├─ 不批次改寫
├─ 仍依原本的歷史規則解讀
└─ 異常跳號保留原值,供後續追查

到這裡我才敢說,這次需求真的不是把 CHAR(3) 改成 CHAR(4)。

至於產號集中到資料庫之後,如果兩個 Client 同時取得最後一號再加一,仍可能拿到相同號碼。這個併發問題留到 Day 9 再處理。


測試不能只測最漂亮的新資料

如果只測 0001 → 0002,很容易得到錯誤的安全感。

至少要涵蓋這些邊界:

測試情境 目前值 預期結果
舊制十進位 295 0296
十進位分界 999 1000
舊制十六進位 3E8 1001
舊制含字母 3EA 依十六進位正確換算
數字外觀重疊 400 必須依歷史 Context 判斷為 400 或 1024,不得只看字串
舊制上限 FFF 4096
新制一般值 1001 1002
非法字元 2G5 阻擋並回報異常
空值 NULL/空字串 依首號規則處理或阻擋
原因不明的跳號 異常大值 依規則警告或阻擋
修改歷史資料 舊制三碼 不因新制驗證而失敗

其中 FFF 等於十進位 4095,它的下一號 4096 仍可放進四碼欄位。但系統接近 9999 前,也要先決定溢位後怎麼處理。

不要等號碼真的用完,才在 Production 現場開需求會議。那種會議通常很有臨場感,但不太有品質。


改版後的結果

目前已實際確認的範圍是:新資料可以取得四碼十進位流水號,主檔與明細保存的是完全相同的字串,存檔後重新查詢也不會因為前導零遺失而找不到資料。

舊資料則維持原本的三碼值,不需要先進行大規模資料遷移;目前抽查與已驗證範圍內的舊資料,仍可依既有流程讀取。 至於修改、簽核、報表與匯出,我仍把它們保留在完整的 End-to-End 回歸清單裡;沒有完成的驗證,不應因為程式可以編譯或單一畫面正常就提前宣布通過。

至於 295 → 647 的歷史跳號,我沒有因為完成四碼改版就宣稱已經找出所有原因。這次先保留異常資料與原始證據,並把新資料的產號責任集中管理;尚未釐清的歷史問題,繼續留在待追查清單裡。

這個結果或許不如「一次把所有問題修乾淨」漂亮,卻更符合 Legacy System 維護的現實:已確認的先修正,未知的保留證據,不用新的改版覆蓋舊問題。


不會 Delphi,也能帶走什麼?

這個案例不只適用於 Delphi 或 ERP。民國年改西元年、固定代碼改 UUID、電話號碼增加區碼,都會遇到類似問題。

可以帶走的核心很簡單:格式改版時,別只檢查新值能不能寫入,還要追蹤它如何穿過整條資料流,並確認歷史資料是否仍能被原本的功能理解。

新規則管理新資料,不應讓歷史資料從此變成不可維護。


上線前的最小檢查清單

  • [ ] 已確認歷史切換條件與流水號作用域,不只依三碼字串判斷格式。
  • [ ] 已找出空值、非法字元、400 這類語意重疊值與原因不明的跳號。
  • [ ] 已確認 Dataset、Query Parameter、暫存變數與共用函式不會截斷或改寫流水號。
  • [ ] 已確認主檔與明細取得完全相同的四碼字串,查詢與排序使用共同數值。
  • [ ] 已測試 295、999、3E8、400、FFF、上限值與非法資料。
  • [ ] 上線前須走完新增、儲存、重查、修改、簽核、報表及歷史資料讀取流程。
  • [ ] 已確認新規則不會阻擋舊資料,並準備停用新流程與回復版本的方法。

今日小結

這次真正修改的不是:

CHAR(3) → CHAR(4)

而是:

舊制混合規則
        ↓
共同數值
        ↓
新制十進位語意

AI 能快速列出影響範圍、整理轉換程式與產生測試案例,但工程師仍要回答:

這段資料過去代表什麼?從哪裡開始改變?舊資料又要如何繼續活下去?

如果這三題沒有答案,修改欄位只是把不確定性放進更大的容器。

💡 今日金句:欄位只多一碼,真正要修改的卻是整套系統對這段資料的理解。

最後留一題,看看大家會站哪一邊:

如果 Production 已經有一筆來源不明的跳號,你會怎麼做?

A. 直接阻擋新增
B. 警告後允許繼續
C. 先沿用現況,再追查歷史資料

這次我的選擇比較接近 C:先保留現況與證據,再追查原因。換成你,會怎麼選?

下一篇,我們會直擊產號流程裡最容易在測試時正常、上線後才爆開的情境:

兩個人同時按下新增,為什麼會拿到同一個號碼?

下一篇,我們就來處理這個留在產號流程裡的問題。


上一篇
Day 7|真正難懂的不是程式碼,而是沒人寫下來的商業規則
下一篇
Day9 | 兩個人同時按新增,為什麼會拿到同一個號碼?從 Legacy Code 看 Race Condition
系列文
AI 救得了祖傳系統嗎?30 天實戰企業 Legacy System × AI 協作開發 共 11 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言